import { isBoolean, isEmptyObject, isFunction, isObject, isString, isArray } from "./type"
import { isHttp } from "./regexp";
import { showLoading, hideLoading } from "./tools";
// import CryptoJS from "crypto-js";

export default class Request {

  /**
   * 构造函数
   * @param {Object} options 全局静态配置信息
   * @prop {String} options.secretKey 秘钥
   * @prop {String} options.requestKey 加密时服务端接收的key值
   * @prop {Boolean} options.printLogger 是否打印请求日志
   */
  constructor(options) {
    const { requestKey, secretKey, printLogger } = options;
    // this.base64 = CryptoJS.enc.Utf8.parse(secretKey);
    this.requestKey = requestKey;
    this.printLogger = printLogger;
  }
  
  // 默认配置
  config = {
    loading: true,  // 是否显示加载弹窗
    sync: true,   // 是否同步发送
    syncType: 'current',   // 同步发送处理类型，current:不发送当前请求，prev:取消前面请求，发送当前请求（注意：prev会取消所有接口相同的请求）
    encrypt: false,  // 是否加密，加密配置在初始化时配置
    type: 'form',  // 请求方式：form、json
    retryNum: 0,   // 失败重试次数， 默认不重试
    retryTime: 1000  // 失败重试间隔，默认1s
  }

  // 当前所有处于请求的接口
  requestTasks = {};

  /**
   * 拦截器
   * @property {Function} request 请求拦截器 
   * @property {Function} response 响应拦截器 
   */
  interceptor = {
    /**
     * 请求拦截器，传递一个回调函数，会往回调函数传递配置对象及取消方法。
     * @param {Function} fn 回调函数，可对配置修改，或者取消请求
     */
    request: (fn) => {
      if(isFunction(fn)) this.requestBeforeFun = fn;
    },

    /**
     * 响应拦截器，传递两个回调函数，会往回调函数传递响应对象。
     * @param {*} successCB 请求成功回调
     * @param {*} failCB 请求失败回调
     */
    response: (successCB, failCB) => {
      if(isFunction(successCB)) this.requestSuccessFun = successCB;
      if(isFunction(failCB)) this.requestFailFun = failCB;
    }
  }

  /**
   * 请求之前执行函数，会传递两个参数
   * @param {Object} config 当前的配置
   * @param {Function} cancel 取消请求函数
   * @returns {Object} 修改后的配置
   */
  requestBeforeFun(config) {
    return config;
  }

  /**
   * 请求成功执行，传递响应对象
   * @param {Object} response 响应对象
   * @returns {Object} 处理后的响应对象
   */
  requestSuccessFun(response) {
    return response;
  }

  /**
   * 请求失败执行，传递响应对象
   * @param {Object} response 响应对象
   * @returns {Object} 处理后的响应对象
   */
  requestFailFun(response) {
    return response;
  }

  /**
   * 全局配置
   * @param {Function} fn 配置函数，传递默认配置
   */
  setConfig(fn) {
    if(isFunction(fn)) this.config = fn(this.config);
  }

  /**
   * 自定义响应验证函数，返回true，进入成功回调。返回false，进入失败回调
   * @param {*} response 响应对象
   * @returns {Boolean} 返回true，进入成功回调。返回false，进入失败回调
   */
  responseValidate(response) {
    return response.statusCode === 200;
  }

  /**
   * 取消加载loading
   */
  cancelLoading() {
    hideLoading();
    hideLoading();
    uni.hideLoading();
    uni.hideLoading();
    uni.stopPullDownRefresh();
    uni.stopPullDownRefresh();
  }

  /**
   * 将对象格式化为参数字符串
   * @param {Object} params 要格式化的对象
   * @returns {String} 
   */
  qs(params) {
    let arr = [];
    for(let key in params) {
      const value = encodeURIComponent(params[key]);
      arr.push(key + '=' + value);
    }
    return arr.join('&');
  }

  /**
   * 加密字符串
   * @param {String} text 要加密的字符串
   * @returns {String} 加密后的字符串
   */
  encrypt(text) {
    let encrypt = CryptoJS.TripleDES.encrypt(text, this.base64, {
      // iv: CryptoJS.enc.Utf8.parse('12345679'),//iv偏移量
      // mode: CryptoJS.mode.CBC,   //CBC模式
      mode: CryptoJS.mode.ECB,      //ECB模式
      padding: CryptoJS.pad.Pkcs7,  //padding处理
    });
    return encrypt.toString(); //加密完成后，转换成字符串
  }

  /**
   * 解密字符串
   * @param {*} text 要解密的字符串
   * @returns 解密后的字符串
   */
  decrypt(text) {
    var decrypt = CryptoJS.TripleDES.decrypt(text, this.base64, {
      mode: CryptoJS.mode.ECB,
      padding: CryptoJS.pad.Pkcs7,
    });
    return decrypt.toString(CryptoJS.enc.Utf8);
  }

  /**
   * 发送请求基类
   * @param {Object} options 配置对象
   * @param {'upload'|'request'} t 要构建的请求类型   文件上传|普通请求
   * @returns {Promise} 在Promise上扩展了 abort 方法，用于取消已经发出的请求
   */
  requestBase(options, t = 'request') {
    const config = Object.assign({}, this.config, options);
    const { 
      baseUrl, 
      url, 
      loading, 
      sync, 
      syncType, 
      encrypt, 
      type, 
      retryNum, 
      retryTime, 
      retryFailCB,
      params,
      header, 
      progressUpdate,
      ...reqConf
    } = config;
    // 网址拼接
    const margeUrl = !isHttp(url) ? baseUrl + url : url;
    if(isObject(params) && !isEmptyObject(params)) {
      const qs = this.qs(params);
      reqConf.url = margeUrl + (margeUrl.indexOf("?") === -1 ? `?${qs}` : `&${qs}`);
    } else {
      reqConf.url = margeUrl;
    }

    // 设置请求类型
    if(t === 'request') {
      const contentTypes = {
        form: 'application/x-www-form-urlencoded',
        json: 'content-type": "application/json;charset=UTF-8',
      }
      const contentType = contentTypes[type];
      reqConf.header = Object.assign({}, { 'content-type': contentType || contentTypes['form'] }, header);
    }
    
    // 当前请求
    let requestTask;
    // 请求次数
    let requestCount = 0;
    const promise = new Promise((resolve, reject) => {

      // 取消请求逻辑
      let isCancel = false;
      const cancel = (text = '取消发送', config = reqConf) => {
        reject({
          errMsg: text,
          config: config
        });
        isCancel = true;
      }

      // 执行拦截器，并对结果处理
      let handleConf = { ...this.requestBeforeFun(reqConf, cancel) };
      if(isCancel) return;
      
      // 同步发送请求判断
      if(sync) {
        if(this.requestTasks[margeUrl] && this.requestTasks[margeUrl].length) {
          if(syncType !== 'prev') {   // 取消当前请求
            cancel('请勿重复提交！');
            return;
          } else {
            // 取消之前的请求
            this.cancelUrlRequest(margeUrl);
          }
        }
      }

      // 请求加密
      if(encrypt) {
        const key = t === 'upload' ? 'formData' : 'data';
        handleConf[key] = { [this.requestKey]: this.encrypt(JSON.stringify(handleConf[key] || {})) };
      }

      // 请求完成
      handleConf.complete = (response) => {
        const currentTasks = this.requestTasks[margeUrl];
        if(isArray(currentTasks)) {
          const idx = currentTasks.indexOf(requestTask);
          if(idx > -1) currentTasks.splice(idx, 1);
          if(!currentTasks.length) delete this.requestTasks[margeUrl];
        }
        
        response.config = handleConf;

        // 响应解密
        if(encrypt && !isObject(response.data) && !isArray(response.data)) {
          try {
            const dataText = this.decrypt(response.data);
            // 尝试解析成json
            try {
              response.data = JSON.parse(dataText);
            } catch (err) {
              response.data = dataText;
            }
          } catch(err) {}
        }

        // 打印响应日志
        if(this.printLogger) {
          // #ifndef APP-PLUS
          console.groupCollapsed
          // #endif
          // #ifdef APP-PLUS
          console.log
          // #endif
          (
            "%c接收：%c" + margeUrl + " " + (response.statusCode || '未发送成功'),
            "color: green",
            this.responseValidate(response) ? "color:green" : "color:red"
          );
          console.log("返回数据：", response.data);
          console.log("响应对象：", response);
          // #ifndef APP-PLUS
          console.groupEnd();
          // #endif
        }

        // 成功
        if (this.responseValidate(response)) {
          response = this.requestSuccessFun(response);
          resolve(response);
          this.cancelLoading();

        // 失败
        } else {
          response = this.requestFailFun(response);
          reject(response);

          // 失败重试
          if(retryNum + 1 > requestCount) {

            // 将定时器绑定到请求任务上，方便清除请求时清除定时器
            requestTask.setTime = setTimeout(() => {
              sendRequest();
            }, retryTime);
          
          // 尝试次数用完仍失败，调用失败回调函数（注意：不是Promise的失败）
          } else {
            if(isFunction(retryFailCB)) retryFailCB(response);
            this.cancelLoading();
          }
        }
      };

      /**
       * 根据处理好的配置发送请求
       */
      var sendRequest = () => {
        requestCount ++;
        /* loading，第一次请求时显示，重试时不再显示（因为没有隐藏），重试完成会隐藏 */
        if(loading && requestCount === 1) {
          let loadingConf = false;
          const title = t === 'upload' ? '上传中...' : '请稍后...';
          if(isBoolean(loading)) loadingConf = { title, mask: true }; 
          if(isString(loading)) loadingConf = { title: loading, mask: true };
          if(isObject(loading)) loadingConf = Object.assign({}, { title, mask: true }, loadingConf); 
          if(loadingConf) showLoading(loadingConf);
        }

        // 发送请求
        if(t === 'upload') {
          requestTask = uni.uploadFile(handleConf);

          // 上传进度监听
          if(isFunction(progressUpdate)) {
            requestTask.onProgressUpdate(progressUpdate);
          }
        } else {
          let requestOptions = handleConf;
          try {
            if(type === 'json') requestOptions = { ...handleConf, data: JSON.stringify(handleConf.data)};
          } catch(e) {}
          requestTask = uni.request(requestOptions);
        }

        // 添加到请求列表
        if(!this.requestTasks[margeUrl]) this.requestTasks[margeUrl] = [requestTask];
        else this.requestTasks[margeUrl].push(requestTask);

        // 打印请求日志
        if(this.printLogger) {
          // #ifndef APP-PLUS
          console.groupCollapsed
          // #endif
          // #ifdef APP-PLUS
          console.log
          // #endif
          (
            "%c请求：%c" + margeUrl + " " + (t === 'upload' ? 'POST' : handleConf.method),
            "color: red",
            "color: orange"
          );
          console.log("请求数据：", t === 'upload' ? handleConf.formData : handleConf.data);
          console.log("携带数据：", params || '无');
          console.log("全部数据：", handleConf);
          // #ifndef APP-PLUS
          console.groupEnd();
          // #endif
        }
      }

      // 执行发送请求
      sendRequest();
    });

    // 在Promise上添加取消请求方法，可以使用 Promise.abort() 直接取消请求
    promise.abort = () => {
      if(isFunction(requestTask.abort)) requestTask.abort();
      if(requestTask.setTime) clearTimeout(requestTask.setTime);
    }

    return promise;
  }


  /**
   * 发送请求
   * @param {Object} options 配置对象
   * @prop {String} options.url	开发者服务器接口地址	
   * @prop {Object/String/ArrayBuffer} options.data 请求的参数。	  App（自定义组件编译模式）不支持ArrayBuffer类型
   * @prop {Object} options.params 拼接参数， 用 ? 连接到url后面
   * @prop {Object} options.header 设置请求的 header，header 中不能设置 Referer。	App、H5端会自动带上cookie，且H5端不可手动修改
   * @prop {String} options.method 请求方法	默认 GET
   * @prop {Number} options.timeout	超时时间，单位 ms。	H5、APP、微信小程序、支付宝小程序。默认 60000
   * @prop {String} options.dataType 如果设为 json，会尝试对返回的数据做一次 JSON.parse	 默认 json
   * @prop {String} options.responseType 设置响应的数据类型。合法值：text、arraybuffer	App和支付宝小程序不支持  默认 text
   * @prop {Boolean} options.sslVerify 验证 ssl 证书	仅App安卓端支持  默认 true
   * @prop {Boolean} options.withCredentials 	跨域请求时是否携带凭证（cookies）	仅H5支持   默认 false
   * @prop {Boolean} options.firstIpv4  DNS解析时优先使用ipv4	仅 App-Android   默认 false
   * @prop {Function} options.success 收到开发者服务器成功返回的回调函数	
   * @prop {Function} options.fail 接口调用失败的回调函数	
   * @prop {Function} options.complete 接口调用结束的回调函数（调用成功、失败都会执行）
   * @prop {Boolean|String|Object} options.loading 传递Boolean控制是否显示、传入String控制显示并设置文字、传入对象控制uni.showLoading配置
   * @prop {Boolean} options.sync  是否使用同步发送
   * @prop {String|'cancelPrev'} options.syncType  使用同步类型   取消本次请求|取消上一个未完成请求  默认 取消本次请求
   * @prop {Boolean} options.encrypt   是否加密
   * @prop {'form' | 'json'} options.type  请求方式，支持form和json，其他方式请在header头里自行设置(覆盖此配置),  默认form
   * @prop {Number} options.retryNum 失败重试次数，仅在fail时执行。默认0，不重试
   * @prop {Number} options.retryTime 重试间隔
   * @prop {Function} options.retryFailCB 失败执行，未设置重试次数时建议使用Promise处理，此方法主要用于重试完成仍失败时执行。
   * @returns {Promise} 在Promise上扩展了 abort 方法，用于取消已经发出的请求
   */
  request(options) {
    return this.requestBase(options, 'request');
  }

  get(url, options = {}) {
    return this.request({
      url,
      method: "GET",
      ...options,
    });
  }

  post(url, data, options = {}) {
    return this.request({
      url,
      data,
      method: "POST",
      ...options,
    });
  }

  // #ifndef MP-ALIPAY
  put(url, data, options = {}) {
    return this.request({
      url,
      data,
      method: "PUT",
      ...options,
    });
  }
  // #endif

  // #ifndef MP-ALIPAY || MP-TOUTIAO
  delete(url, data, options = {}) {
    return this.request({
      url,
      data,
      method: "DELETE",
      ...options,
    });
  }
  // #endif

  // #ifdef H5 || MP-WEIXIN
  connect(url, data, options = {}) {
    return this.request({
      url,
      data,
      method: "CONNECT",
      ...options,
    });
  }
  // #endif

  // #ifdef H5 || MP-WEIXIN || MP-BAIDU
  head(url, data, options = {}) {
    return this.request({
      url,
      data,
      method: "HEAD",
      ...options,
    });
  }
  // #endif

  // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
  options(url, data, options = {}) {
    return this.request({
      url,
      data,
      method: "OPTIONS",
      ...options,
    });
  }
  // #endif

  // #ifdef H5 || MP-WEIXIN
  trace(url, data, options = {}) {
    return this.request({
      url,
      data,
      method: "TRACE",
      ...options,
    });
  }
  // #endif
  
  
  /**
   * 上传文件
   * @param {Object} options 配置对象
   * @prop {String} args.url 开发者服务器 url
   * @prop {Array<File>} args.files 需要上传的文件列表。使用 files 时，filePath 和 name 不生效。  App、H5
   * @prop {String} args.fileType 文件类型，image/video/audio， 仅支付宝小程序，且必填。
   * @prop {File} args.file 要上传的文件对象。  仅H5支持
   * @prop {String} args.filePath 要上传文件资源的路径。
   * @prop {String} args.name 文件对应的 key , 开发者在服务器端通过这个 key 可以获取到文件二进制内容
   * @prop {Object} args.header HTTP 请求 Header, header 中不能设置 Referer。
   * @prop {Number} args.timeout 超时时间，单位 ms   App、H5
   * @prop {Object} args.formData HTTP 请求中其他额外的 form data
   * @prop {Function} args.success 成功回调（不建议使用，建议直接使用Promise处理）
   * @prop {Function} args.fail 失败回调（不建议使用，建议直接使用Promise处理）
   * @prop {Function} args.complete 完成回调（不建议使用，建议直接使用Promise处理）
   * @prop {Boolean|String|Object} options.loading 传递Boolean控制是否显示、传入String控制显示并设置文字、传入对象控制uni.showLoading配置
   * @prop {Boolean} options.sync  是否使用同步发送
   * @prop {String|'cancelPrev'} options.syncType  使用同步类型   取消本次请求|取消上一个未完成请求  默认 取消本次请求
   * @prop {Boolean} options.encrypt   是否加密
   * @prop {Number} options.retryNum 失败重试次数，仅在fail时执行。默认0，不重试
   * @prop {Number} options.retryTime 重试间隔
   * @prop {Function} options.progressUpdate 上传进度监听函数
   * @prop {Function} options.retryFailCB 失败执行，未设置重试次数时建议使用Promise处理，此方法主要用于重试完成仍失败时执行。
   * @returns {Promise} 在Promise上扩展了 abort 方法，用于取消已经发出的请求
   */
  upload(options) {
    return this.requestBase(options, 'upload');
  }
  
  /**
   * 取消指定接口的全部请求
   * @param {String} url 要取消的接口名
   */
  cancelUrlRequest(url) {
    const tasks = this.requestTasks[url];
    if(isArray(tasks)) {
      for(let i = tasks.length - 1; i >= 0; i--) {
        const task = tasks[i];
        if(isFunction(task.abort)) task.abort();
        if(task.setTime) clearTimeout(task.setTime);
      }
    }
  }

  /**
   * 取消全部请求
   */
  cancelAllRequest() {
    for(let key in this.requestTasks) {
      this.cancelUrlRequest(key);
    }
  }
  
}
